Python: fix(python): prevent local tool approvals from serializing as MCP responses - #8088
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Approval correlation mishandles cross-message ordering and reused call IDs, risking duplicate execution or lost pending approvals.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Fixes local approval leakage into OpenAI MCP payloads and makes approval filtering order-independent.
Changes:
- Restricts MCP serialization to hosted approvals.
- Reworks approval resolution and filtering.
- Adds regression coverage for approval ordering and serialization.
File summaries
| File | Description |
|---|---|
python/packages/openai/agent_framework_openai/_chat_client.py |
Guards hosted approval serialization. |
python/packages/openai/tests/openai/test_openai_chat_client.py |
Tests local and hosted serialization behavior. |
python/packages/core/agent_framework/_tools.py |
Reworks approval collection and filtering. |
python/packages/core/agent_framework/_sessions.py |
Makes history filtering order-independent. |
python/packages/core/tests/core/test_tools.py |
Adds approval filtering regressions. |
python/packages/core/tests/core/test_sessions.py |
Tests history approval resolution. |
python/packages/core/tests/core/test_agent_executor.py |
Formatting-only cleanup. |
python/packages/core/tests/core/test_function_invocation_logic.py |
Formatting-only cleanup. |
docs/specs/004-python-function-calling-loop.md |
Formatting-only specification edits. |
Review details
Suppressed comments (1)
python/packages/core/agent_framework/_tools.py:2447
- This global call-id check marks a fresh pending request as answered if any older occurrence with the same
call_idhas a terminal result._remove_unanswered_approval_batches_from_model_inputthen fails to recognize and hide that pending batch, even though call-id reuse after completion is supported (python/packages/core/AGENTS.md:173-179). Pair terminal events with request occurrences instead of applying them to every request sharing the id.
if content.id in answered_approval_ids or function_call.call_id in resolved_call_ids:
continue
- Files reviewed: 9/9 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
31e0dd2 to
e0bf77d
Compare
e378351 to
f6ab166
Compare
|
Evan Mattson (@moonbox3) The latest push fixes the CI failures. Could you please re-run the failed checks or let me know if there's anything else to address? Thanks! |
Motivation & Context
Approved local (non-MCP) tool calls fail with a hard 400 error from the OpenAI Responses API when using
approval_mode="always_require"with service-side storage. This blocks any agent that relies on local gated tools from completing their turn.The bug requires two independent defects to manifest, and fixing either prevents the 400. This PR addresses both to ensure robust, order-independent behavior.
Description & Review Guide
What are the major changes?
_chat_client.py): Added an_is_hosted_tool_approvalguard to bothfunction_approval_requestandfunction_approval_responsearms in_prepare_content_for_openai. Local approvals now return{}and are safely filtered out upstream. Also restored therequest_uses_service_side_storageguard on the response arm to prevent orphaned responses under storage._sessions.py,_tools.py): Replaced the fragile, order-dependent single-pass queue-popping logic with a robust two-pass, set-based approach across four functions:_approval_controls_to_keep,_collect_approval_responses,_collect_unanswered_approval_requests, and_remove_unanswered_approval_batches_from_model_input. Pass 1 collects all resolvedcall_ids regardless of position; Pass 2 filters using the pre-built set. This eliminates the ordering bug where a terminal result appearing before an approval response (the "approval-resume" layout) caused the resolved response to leak into model input.What is the impact of these changes?
What do you want reviewers to focus on?
_approval_controls_to_keep— verify the semantic boundary between what session history filtering resolves (terminal results + matching responses) vs. what current-turn collectors resolve._is_hosted_tool_approvalguard placement in_chat_client.py— confirm returning{}for local approvals is safe given all upstream callers useif prepared:.Related Issue
Fixes #7452
related PR #7473
Contribution Checklist